1//////////////////////////////////////////////////////////////////////
  2// LibFile: masks2d.scad
  3//   This file provides 2D masking shapes that you can use with {{edge_profile()}} to mask edges.
  4//   The shapes include the simple roundover and chamfer as well as more elaborate shapes
  5//   like the cove and ogee found in furniture and architecture.  You can make the masks
  6//   as geometry or as 2D paths.
  7// Includes:
  8//   include <BOSL2/std.scad>
  9// FileGroup: Basic Modeling
 10// FileSummary: 2D masking shapes for edge profiling: including roundover, cove, teardrop, ogee.
 11// FileFootnotes: STD=Included in std.scad
 12//////////////////////////////////////////////////////////////////////
 13
 14
 15function _inset_corner(corner, mask_angle, inset, excess, flat_top) =
 16    let(
 17        vertex = [inset.x/sin(mask_angle)+inset.y/tan(mask_angle), inset.y],
 18        corner = move(vertex, corner), 
 19        outside = [
 20                     [corner[2].x,-excess],
 21                     [-(excess)/tan(mask_angle/2), -excess],
 22                     if (!flat_top) corner[0] + polar_to_xy(inset.x+excess,90+mask_angle),
 23                     if (flat_top) corner[0] - [(excess+inset.x)/sin(mask_angle),0]
 24                  ]
 25    )
 26    [outside, corner];
 27    
 28
 29
 30// Section: 2D Masking Shapes
 31
 32// Function&Module: mask2d_roundover()
 33// Synopsis: Creates a circular mask shape for rounding edges or beading.
 34// SynTags: Geom, Path
 35// Topics: Shapes (2D), Paths (2D), Path Generators, Attachable, Masks (2D)
 36// See Also: corner_profile(), edge_profile(), face_profile(), fillet()
 37// Usage: As module
 38//   mask2d_roundover(r|d=|h=|height=|cut=|joint=, [inset], [mask_angle], [excess], [flat_top=], [quarter_round=]) [ATTACHMENTS];
 39// Usage: As function
 40//   path = mask2d_roundover(r|d=|h=|height=|cut=|joint=, [inset], [mask_angle], [excess], [flat_top=], [quarter_round=]);
 41// Description:
 42//   Creates a 2D roundover/bead mask shape that is useful for extruding into a 3D mask for an edge.
 43//   Conversely, you can use that same extruded shape to make an interior fillet between two walls.
 44//   As a 2D mask, this is designed to be differenced away from the edge of a shape that with its corner at the origin and one edge on the X+ axis and the other mask_angle degrees counterclockwise from the X+ axis.  
 45//   If called as a function, returns a 2D path of the outline of the mask shape.
 46//   .
 47//   The roundover can be specified by radius, diameter, height, cut, or joint length.
 48//   ![Types of Roundovers](images/rounding/figure_1_1.png)
 49//   .
 50//   If you need roundings to agree on edges of different mask_angle, e.g. to round the base of a prismoid, then you need all of the
 51//   masks used to have the same height.  (Note that it may appear that matching joint would also work, but it does not because the joint distances are measured
 52//   in different directions.)  You can get the same height by setting the `height` parameter, which is an alternate way to control the size of the rounding.
 53//   You can also set `quarter_round=true`, which creates a rounding that uses a quarter circle of the specified radius for all mask angles.  If you have set inset
 54//   you will need `flat_top=true` as well.  Note that this is the default if you use `quarter_round=true` but not otherwise.  Generally if you want a roundover
 55//   results are best using the `height` option but if you want a bead as you get using `inset` the results are often best using the `quarter_round=true` option. 
 56// Arguments:
 57//   r = Radius of the roundover.
 58//   inset = Optional bead inset size, perpendicular to the two edges.  Scalar or 2-vector.  Default: 0
 59//   mask_angle = Number of degrees in the corner angle to mask.  Default: 90
 60//   excess = Extra amount of mask shape to creates on the X and quasi-Y sides of the shape.  Default: 0.01
 61//   ---
 62//   d = Diameter of the roundover.
 63//   h / height = Mask height excluding inset and excess.  Give instead of r / d, cut or joint when you want a consistent mask height, no matter what the mask angle.
 64//   cut = Cut distance.  IE: How much of the corner to cut off.  See [Types of Roundovers](rounding.scad#section-types-of-roundovers).
 65//   joint = Joint distance.  IE: How far from the edge the roundover should start.  See [Types of Roundovers](rounding.scad#section-types-of-roundovers).
 66//   flat_top = If true, the top inset of the mask will be horizontal instead of angled by the mask_angle.  Default: true if quarter_round is set, false otherwise.
 67//   quarter_round = If true, make a roundover independent of the mask_angle, defined based on a quarter circle of the specified size.  Creates mask with angle-independent height.  Default: false.
 68//   anchor = Translate so anchor point is at origin (0,0,0).  See [anchor](attachments.scad#subsection-anchor).  Default: `CENTER`
 69//   spin = Rotate this many degrees around the Z axis after anchor.  See [spin](attachments.scad#subsection-spin).  Default: `0`
 70// Side Effects:
 71//   Tags the children with "remove" (and hence sets `$tag`) if no tag is already set.
 72//
 73// Example(2D): 2D Roundover Mask by Radius
 74//   mask2d_roundover(r=10);
 75// Example(2D): 2D Bead Mask
 76//   mask2d_roundover(r=10,inset=2);
 77// Example(2D): 2D Roundover Mask by Radius, acute angle
 78//   mask2d_roundover(r=10, mask_angle=50);
 79// Example(2D): 2D Bead Mask by Radius, acute angle
 80//   mask2d_roundover(r=10, inset=2, mask_angle=50);
 81// Example(2D): 2D Bead Mask for obtuse angle, by height
 82//   mask2d_roundover(h=10, inset=2, mask_angle=135, $fn=64);
 83// Example(2D): 2D Bead Mask for obtuse angle, by height with flat top
 84//   mask2d_roundover(h=10, inset=2, mask_angle=135, flat_top=true, $fn=64);
 85// Example(2D): 2D Angled Bead Mask by Joint Length.  Joint length does not include the inset.  
 86//   mask2d_roundover(joint=10, inset=2, mask_angle=75);
 87// Example(2D): Increasing the Excess
 88//   mask2d_roundover(r=10, inset=2, mask_angle=75, excess=2);
 89// Example(2D): quarter_round bead on an acute angle
 90//   mask2d_roundover(r=10, inset=2, mask_angle=50, quarter_round=true);
 91// Example(2D): quarter_round bead on an obtuse angle
 92//   mask2d_roundover(r=10, inset=2, mask_angle=135, quarter_round=true);
 93// Example: Masking by Edge Attachment
 94//   diff()
 95//   cube([50,60,70],center=true)
 96//       edge_profile([TOP,"Z"],except=[BACK,TOP+LEFT])
 97//           mask2d_roundover(h=12, inset=2);
 98// Example: Making an interior fillet
 99//   %render() difference() {
100//       move(-[5,0,5]) cube(30, anchor=BOT+LEFT);
101//       cube(310, anchor=BOT+LEFT);
102//   }
103//   xrot(90)
104//       linear_extrude(height=30, center=true)
105//           mask2d_roundover(r=10);
106// Example(3D,Med,VPT=[25,30,12],VPR=[68,0,12],VPD=180): Rounding over top of an extreme prismoid using height option
107//   diff()
108//     prismoid([30,20], [50,60], h=20, shift=[40,50])
109//        edge_profile(TOP, excess=27)
110//           mask2d_roundover(height=5, mask_angle=$edge_angle, $fn=128);
111// Example(3D,Med,VPT=[25,30,12],VPR=[68,0,12],VPD=180): Using the quarter_round option results in a lip on obtuse angles, so it may not be the best choice for pure roundings.  
112//   diff()
113//     prismoid([30,20], [50,60], h=20, shift=[40,50])
114//        edge_profile(TOP, excess=27)
115//           mask2d_roundover(r=5, mask_angle=$edge_angle, quarter_round=true, $fn=128);
116// // Example(3D,Med,VPT=[25,30,12],VPR=[68,0,12],VPD=180): Can improve the quarter round option by using it only for acute angles and falling back on regular rounding for obtuse angles. Note that in this case, obtuse angles are fully rounded, but acute angles still have a corner, but one that is not as sharp as the original angle.  
117//   diff()
118//     prismoid([30,20], [50,60], h=20, shift=[40,50])
119//        edge_profile(TOP, excess=27)
120//           mask2d_roundover(r=5, mask_angle=$edge_angle, quarter_round=$edge_angle<90, $fn=32);
121// Example(3D,Med,VPT=[25,30,12],VPR=[68,0,12],VPD=180): Creating a bead on the prismoid using the height option with flat_top=true:
122//   diff()
123//     prismoid([30,20], [50,60], h=20, shift=[40,50])
124//        edge_profile(TOP, excess=27)
125//           mask2d_roundover(height=5, mask_angle=$edge_angle, inset=1.5, flat_top=true, $fn=128);
126// Example(3D,Med,VPT=[25,30,12],VPR=[68,0,12],VPD=180): Bead may be more pleasing using the quarter_round option, with curves terminating in a plane parallel to the prismoid top.  The size of the inset edge will be larger than requested when the angle is obtuse.  
127//   diff()
128//     prismoid([30,20], [50,60], h=20, shift=[40,50])
129//        edge_profile(TOP, excess=27)
130//           mask2d_roundover(r=5, mask_angle=$edge_angle, quarter_round=true, inset=1.5, $fn=128);
131module mask2d_roundover(r, inset=0, mask_angle=90, excess=0.01, flat_top, d, h, height, cut, quarter_round=false, joint, anchor=CENTER,spin=0) {
132    path = mask2d_roundover(r=r, d=d, h=h, height=height, cut=cut, joint=joint, inset=inset,
133                            flat_top=flat_top, mask_angle=mask_angle, excess=excess, quarter_round=quarter_round);
134    default_tag("remove") {
135        attachable(anchor,spin, two_d=true, path=path) {
136            polygon(path);
137            children();
138        }
139    }
140}
141
142
143function mask2d_roundover(r, inset=0, mask_angle=90, excess=0.01, flat_top, quarter_round=false, d, h, height, cut, joint, anchor=CENTER, spin=0) =
144    assert(one_defined([r,height,d,h,cut,joint],"r,height,d,h,cut,joint"))
145    assert(all_nonnegative([excess]), "excess must be a nonnegative value")
146    assert(is_finite(mask_angle) && mask_angle>0 && mask_angle<180)
147    assert(is_finite(inset)||is_vector(inset,2))
148    assert(is_bool(quarter_round))
149    let(flat_top=default(flat_top, quarter_round))
150    assert(is_bool(flat_top))
151    let(
152        inset = is_list(inset)? inset : [inset,inset],
153        r = get_radius(r=r,d=d,dflt=undef),
154        dummy2=assert(is_def(r) || !quarter_round,"Must give r / d when quarter_round is true"),
155        h = u_add(one_defined([h,height],"h,hight",dflt=undef),flat_top || mask_angle>=90?0:-inset.x*cos(mask_angle)),        
156        // compute [joint length, radius] for different types of input
157        jr = is_def(h) ? assert(all_positive([h]), "height / h must be larger than y inset")
158                         h/sin(mask_angle)*[1,tan(mask_angle/2)]
159           : is_def(r) ?  assert(all_positive([r]), "r / d must be a positive value")
160                          [r/tan(mask_angle/2), r]
161           : is_def(joint) ? assert(all_positive([joint]), "joint must be a positive value")
162                             joint*[1, tan(mask_angle/2)]
163           : assert(all_positive([cut]),"cut must be a positive value")
164             let(circ_radius=cut/(1/sin(mask_angle/2)-1))
165             [circ_radius/tan(mask_angle/2), circ_radius],
166        dist=jr[0],
167        radius=jr[1],
168        quarter_round_top = approx(mask_angle,90) ? 0
169                  : radius/tan(mask_angle),
170        extra = radius/20,  // Exact solution is tangent, which will make bad geometry, so insert an offset factor
171        quarter_round_shift = !quarter_round || mask_angle<=90 ? 0
172                    : radius/sin(180-mask_angle)-radius+extra,
173        outside_corner = _inset_corner(
174                            quarter_round ?
175                            [
176                              [quarter_round_top,radius],
177                              [0,0],
178                              [radius+quarter_round_top+quarter_round_shift,0]
179                              ]
180                           :
181                            [
182                              dist*[cos(mask_angle),sin(mask_angle)],
183                              [0,0],
184                              [dist,0]
185                            ],
186                            mask_angle, inset, excess, flat_top),
187        // duplicates arise at one or both ends if excess and inset are both zero there
188        cornerpath = !quarter_round ? outside_corner[1]
189                   : mask_angle<=90 ? outside_corner[1]+[[0,0],[quarter_round_top,0],[0,0]]
190                   : [ outside_corner[1][0]+[quarter_round_shift,0],
191                       [outside_corner[1][0].x+quarter_round_shift,inset.y],
192                       outside_corner[1][2]
193                     ],
194        dummy=assert(last(cornerpath).x>=0,str("inset.y is too large to fit roundover at angle ",mask_angle)),
195        path = deduplicate([
196                             each outside_corner[0],
197                             outside_corner[1][0],
198                             each arc(corner=cornerpath, r=radius),
199                             outside_corner[1][2]
200                           ]
201                          ,closed=true)
202    ) reorient(anchor,spin, two_d=true, path=path, extent=false, p=path);
203
204
205
206
207// Function&Module: mask2d_teardrop()
208// Synopsis: Creates a 2D teardrop shape with specified max angle from vertical.
209// SynTags: Geom, Path
210// Topics: Shapes (2D), Paths (2D), Path Generators, Attachable, Masks (2D), FDM Optimized
211// See Also: corner_profile(), edge_profile(), face_profile()
212// Usage: As Module
213//   mask2d_teardrop(r|d=, [angle], [inset] [mask_angle], [excess], [cut=], [joint=], [h=|height=]) [ATTACHMENTS];
214// Usage: As Function
215//   path = mask2d_teardrop(r|d=, [angle], [inset], [mask_angle], [excess], [cut=], [joint=], [h=|height=]);
216// Description:
217//   Creates a 2D teardrop mask shape that is useful for extruding into a 3D mask for an edge.
218//   Conversely, you can use that same extruded shape to make an interior teardrop fillet between two walls.
219//   As a 2D mask, this is designed to be differenced away from the edge of a shape that with its corner at the origin and one edge on the X+ axis and the other mask_angle degrees counterclockwise from the X+ axis.  
220//   If called as a function, returns a 2D path of the outline of the mask shape.
221//   This is particularly useful to make partially rounded bottoms, that don't need support to print.
222//   The roundover can be specified by radius, diameter, height, cut, or joint length.
223//   ![Types of Roundovers](images/rounding/figure_1_1.png)
224// Arguments:
225//   r = Radius of the rounding.
226//   angle = The angle from vertical of the flat section.  Must be between mask_angle-90 and 90 degrees.  Default: 45.  
227//   inset = Optional bead inset size perpendicular to edges.  Default: 0
228//   mask_angle = Number of degrees in the corner angle to mask.  Default: 90
229//   excess = Extra amount of mask shape to creates on the X- and Y- sides of the shape. Default: 0.01
230//   ---
231//   d = Diameter of the rounding.
232//   h / height = Mask height excluding inset and excess.  Given instead of r or d when you want a consistent mask height, no matter what the mask angle.
233//   cut = Cut distance.  IE: How much of the corner to cut off.  See [Types of Roundovers](rounding.scad#section-types-of-roundovers).
234//   joint = Joint distance.  IE: How far from the edge the roundover should start.  See [Types of Roundovers](rounding.scad#section-types-of-roundovers).
235//   flat_top = If true, the top inset of the mask will be horizontal instead of angled by the mask_angle.  Default: true.
236//   anchor = Translate so anchor point is at origin (0,0,0).  See [anchor](attachments.scad#subsection-anchor).  Default: `CENTER`
237//   spin = Rotate this many degrees around the Z axis after anchor.  See [spin](attachments.scad#subsection-spin).  Default: `0`
238// Side Effects:
239//  Tags the children with "remove" (and hence sets `$tag`) if no tag is already set.
240// Example(2D): 2D Teardrop Mask
241//   mask2d_teardrop(r=10,$fn=64);
242// Example(2D): 2D Teardrop Mask for acute angle
243//   mask2d_teardrop(r=10, mask_angle=75,$fn=64);
244// Example(2D): 2D Teardrop Mask for obtuse angle, specifying height
245//   mask2d_teardrop(h=10, mask_angle=115,$fn=128);
246// Example(2D): Increasing Excess
247//   mask2d_teardrop(r=10, mask_angle=75, excess=2);
248// Example(2D): Using a Custom Angle
249//   mask2d_teardrop(r=10,angle=30,$fn=128);
250// Example(2D): With an acute mask_angle you can choose an angle of zero:
251//   mask2d_teardrop(r=10,mask_angle=44,angle=0);
252// Example(2D): With an acute mask_angle you can even choose a negative angle
253//   mask2d_teardrop(r=10,mask_angle=44,angle=-15);
254// Example(2D): With an obtuse angle you need to choose a larger angle.  Here we add inset.
255//   mask2d_teardrop(h=10, mask_angle=135,angle=60, inset=2);
256// Example(2D): Same thing with `flat_top=true`.  
257//   mask2d_teardrop(h=10, mask_angle=135,angle=60, inset=2, flat_top=true);
258// Example: Masking by Edge Attachment
259//   diff()
260//   cube([50,60,70],center=true)
261//       edge_profile(BOT)
262//           mask2d_teardrop(r=10, angle=40);
263// Example: Making an interior teardrop fillet
264//   %render() difference() {
265//       move(-[5,0,5]) cube(30, anchor=BOT+LEFT);
266//       cube(310, anchor=BOT+LEFT);
267//   }
268//   xrot(90)
269//       linear_extrude(height=30, center=true)
270//           mask2d_teardrop(r=10);
271
272function mask2d_teardrop(r, angle=45, inset=[0,0], mask_angle=90, excess=0.01, flat_top=false, d, h, height, cut, joint, anchor=CENTER, spin=0) =  
273    assert(one_defined([r,height,d,h,cut,joint],"r,height,d,h,cut,joint"))
274    assert(is_finite(angle) && angle>mask_angle-90 && angle<90)
275    assert(is_finite(mask_angle) && mask_angle>0 && mask_angle<180)
276    assert(all_nonnegative([excess]), "excess must be a nonnegative value")
277    assert(is_finite(inset)||is_vector(inset,2))
278    assert(is_bool(flat_top))
279    let(
280        inset = is_list(inset)? inset : [inset,inset],
281        r = get_radius(r=r,d=d,dflt=undef),
282        h = one_defined([h,height],"h,hight",dflt=undef),
283        // compute [joint length, radius] for different types of input
284        jr = is_def(h) ? assert(all_positive([h]), "height / h must be a positive value")
285                         (flat_top ? (h+inset.x*cos(mask_angle))/sin(mask_angle)*[1,tan(mask_angle/2)]
286                                   : h/sin(mask_angle)*[1,tan(mask_angle/2)])
287           : is_def(r) ?  assert(all_positive([r]), "r / d must be a positive value")
288                          [r/tan(mask_angle/2), r]
289           : is_def(joint) ? assert(all_positive([joint]), "joint must be a positive value")
290                             joint*[1, tan(mask_angle/2)]
291           : assert(all_positive([cut]),"cut must be a positive value")
292             let(circ_radius=cut/(1/sin(mask_angle/2)-1))
293             [circ_radius/tan(mask_angle/2), circ_radius],
294        dist=jr[0],
295        radius=jr[1],
296        outside_corner = _inset_corner(
297                            [
298                              dist*[cos(mask_angle),sin(mask_angle)],
299                              [0,0],
300                              [dist,0]
301                            ],
302                            mask_angle, inset, excess, flat_top),
303
304        arcpts = arc(r=radius, corner=outside_corner[1]),
305        arcpts2 = [
306            for (i = idx(arcpts))
307              if(i==0 || v_theta(arcpts[i]-arcpts[i-1]) <= angle-90)
308                arcpts[i]
309        ],
310        line1 = [last(arcpts2), last(arcpts2) + polar_to_xy(1, angle-90)],
311        line2 = [[0,inset.y], [100,inset.y]],
312        ipt = line_intersection(line1,line2),
313        path = deduplicate([
314                             [ipt.x, -excess],
315                             each select(outside_corner[0],1,-1),
316                             each arcpts2,
317                             ipt
318                           ], closed=true)
319    ) reorient(anchor,spin, two_d=true, path=path, extent=false, p=path);
320    
321
322
323module mask2d_teardrop(r, angle=45, mask_angle=90, excess=0.01, inset=0, flat_top=false, height, d, h, cut, joint, anchor=CENTER, spin=0) {
324    path = mask2d_teardrop(r=r, d=d, h=h, height=height, flat_top=flat_top, cut=cut, joint=joint, angle=angle,inset=inset, mask_angle=mask_angle, excess=excess);
325    default_tag("remove") {
326        attachable(anchor,spin, two_d=true, path=path) {
327            polygon(path);
328            children();
329        }
330    }
331}
332
333
334
335
336// Function&Module: mask2d_cove()
337// Synopsis: Creates a 2D cove (quarter-round) mask shape.
338// SynTags: Geom, Path
339// Topics: Shapes (2D), Paths (2D), Path Generators, Attachable, Masks (2D)
340// See Also: corner_profile(), edge_profile(), face_profile()
341// Usage: As module
342//   mask2d_cove(r|d=|h=|height=, [inset], [mask_angle], [excess], [bulge=], [flat_top=], [quarter_round=]) [ATTACHMENTS];
343// Usage: As function
344//   path = mask2d_cove(r|d=|h=, [inset], [mask_angle], [excess], [bulge=], [flat_top=]);
345// Description:
346//   Creates a 2D cove mask shape that is useful for extruding into a 3D mask for an edge.
347//   Conversely, you can use that same extruded shape to make an interior rounded shelf decoration between two walls.
348//   As a 2D mask, this is designed to be differenced away from the edge of a shape that with its corner at the origin and one edge on the X+ axis and the other mask_angle degrees counterclockwise from the X+ axis.  
349//   If called as a function, returns a 2D path of the outline of the mask shape.
350//   .
351//   If you need coves to agree on edges of different mask_angle, e.g. on the top of a prismoid, then you need all of the
352//   masks used to have the same height.   You can get the same height by setting the `height` parameter.  For obtuse angles, however, the cove mask may not
353//   have is maximum height at the edge, which means it won't mate with adjacent coves.  You can fix this using `flat_top=true` which extends the circle
354//   with a line to maintain a flat top.  Another way to fix it is to set `bulge`.  You can also achieve constant height using the `quarter_round=` option,
355//   which uses a quarter circle of the specified size for all mask_angle values.  This option often produces a nice result because coves all terminate in a
356//   plane at 90 degrees.  
357// Arguments:
358//   r = Radius of the cove.
359//   inset = Optional amount to inset in the perpendicular direction from the edges.  Scalar or 2-vector.  Default: 0
360//   mask_angle = Number of degrees in the corner angle to mask.  Default: 90
361//   excess = Extra amount of mask shape to creates on the X and quasi-Y sides of the shape.  Default: 0.01
362//   ---
363//   d = Diameter of the cove.
364//   h / height = Mask height, excluding inset and excess.  Given instead of r or d when you want a consistent mask height, no matter what the mask angle.
365//   bulge = specify arc as the distance away from a straight line chamfer.  The arc will not meet the sides at a 90 deg angle. 
366//   quarter_round = If true, make cove independent of the mask_angle, defined based on a quarter circle, with angle-independent radius. The mask will have constant height.  Default: false.
367//   flat_top = If true, the top inset of the mask will be horizontal instead of angled by the mask_angle.  In the case of obtuse angles force the mask to have a flat section at its left side instead of a circular arc.  Default: true if quarter_round is set, false otherwise.
368//   anchor = Translate so anchor point is at origin (0,0,0).  See [anchor](attachments.scad#subsection-anchor).  Default: `CENTER`
369//   spin = Rotate this many degrees around the Z axis after anchor.  See [spin](attachments.scad#subsection-spin).  Default: `0`
370// Side Effects:
371//  Tags the children with "remove" (and hence sets `$tag`) if no tag is already set.
372// Example(2D): 2D Cove Mask by Radius
373//   mask2d_cove(r=10);
374// Example(2D): 2D Inset Cove Mask (not much different than a regular cove of larger radius)
375//   mask2d_cove(r=10,inset=3);
376// Example(2D): 2D Cove Mask for acute angle, specified by height, with the bulge set to change the curve.  Note that the circular arc is not perpendicular to the sides.  
377//   mask2d_cove(h=10,mask_angle=55, bulge=3);
378// Example(2D): 2D Cove Mask for obtuse angle, specified by height.  This will produce an odd result if combined with other masks because the maximum height is in the middle.  
379//   mask2d_cove(h=10,mask_angle=145);
380// Example(2D): 2D Cove Mask for obtuse angle with flat top.  This is one solution to the problem of the previous example.  Max height is achieved at the left corner. 
381//   mask2d_cove(h=10,mask_angle=145,flat_top=true);
382// Example(2D): 2D Cove Mask for obtuse angle, specified by height with bulge parameter.  Another way to fix the problem of the previous example: the max height is again achieved at the left corner.  
383//   mask2d_cove(h=10,mask_angle=145, bulge=3, $fn=128);
384// Example(2D): 2D Cove Mask for acute angle with quarter_round enabled
385//   mask2d_cove(r=10,mask_angle=55,quarter_round=true);
386// Example(2D): 2D Cove Mask for obtuse angle, specified by height.  Note that flat_top is on by default in quarter_round mode.  
387//   mask2d_cove(r=10,mask_angle=145,quarter_round=true);
388// Example(2D): Increasing the Excess
389//   mask2d_cove(r=10,inset=3,mask_angle=75, excess=2);
390// Example: Masking by Edge Attachment
391//   diff()
392//   cube([50,60,70],center=true)
393//       edge_profile([TOP,"Z"],except=[BACK,TOP+LEFT])
394//           mask2d_cove(h=10, inset=3);
395// Example: Making an interior rounded shelf
396//   %render() difference() {
397//       move(-[5,0,5]) cube(30, anchor=BOT+LEFT);
398//       cube(310, anchor=BOT+LEFT);
399//   }
400//   xrot(90)
401//       linear_extrude(height=30, center=true)
402//           mask2d_cove(r=5, inset=5);
403// Example(3D,Med): A cove on top of an extreme prismoid top by setting height and using flat_top mode.  This creates **long** flat tops sections at obtuse angles. 
404//   diff()
405//   prismoid([50,60], [20,30], h=20, shift=[25,16])
406//       edge_profile(TOP, excess=20)
407//           mask2d_cove(h=5, inset=0, mask_angle=$edge_angle, flat_top=true, $fn=128);
408// Example(3D,Med): Cove on an extreme prismoid top by setting height and bulge.  Obtuse angles have long **curved** sections.  
409//   diff()
410//   prismoid([50,60], [20,30], h=20, shift=[25,16])
411//       edge_profile(TOP, excess=20)
412//           mask2d_cove(h=5, inset=0, mask_angle=$edge_angle, bulge=1, $fn=128);
413// Example(3D,Med): Rounding an extreme prismoid top using quarter_round.  Another way to handle this situation. 
414//   diff()
415//   prismoid([50,60], [20,30], h=20, shift=[25,16])
416//       edge_profile(TOP, excess=20)
417//           mask2d_cove(r=5, inset=0, mask_angle=$edge_angle, quarter_round=true, $fn=128);
418
419module mask2d_cove(r, inset=0, mask_angle=90, excess=0.01, flat_top, bulge, d, h, height, quarter_round=false, anchor=CENTER, spin=0) {
420    path = mask2d_cove(r=r, d=d, h=h, height=height, bulge=bulge, flat_top=flat_top, quarter_round=quarter_round, inset=inset, mask_angle=mask_angle, excess=excess);
421    default_tag("remove") {
422        attachable(anchor,spin, two_d=true, path=path) {
423            polygon(path);
424            children();
425        }
426    }
427}
428
429
430function mask2d_cove(r, inset=0, mask_angle=90, excess=0.01, flat_top, d, h, height,bulge, quarter_round=false, anchor=CENTER, spin=0) =
431    assert(one_defined([r,d,h,height],"r,d,h,height"))
432    assert(is_finite(mask_angle) && mask_angle>0 && mask_angle<180)
433    assert(is_finite(excess))
434    assert(is_finite(inset)||(is_vector(inset)&&len(inset)==2))
435    assert(is_bool(quarter_round))
436    let(flat_top=default(flat_top,quarter_round))
437    assert(is_bool(flat_top))
438    assert(is_undef(bulge) || all_positive([bulge]),"bulge must be a positive value")
439    let(
440        inset = force_list(inset,2),
441        r = get_radius(r=r,d=d,dflt=undef),
442        h = u_add(one_defined([h,height],"h,hight",dflt=undef),flat_top || mask_angle>=90?0:-inset.x*cos(mask_angle)),
443        radius = is_def(h) ? assert(all_positive([h]), "height / h must be a larger than y inset")
444                             !bulge && (quarter_round || mask_angle>90) ? h-inset.y
445                           : h/sin(mask_angle)
446               : assert(all_positive([r]), "r / d must be a positive value") r,
447        quarter_round_ofs = quarter_round ? radius/tan(mask_angle) : 0,
448        outside_corner = _inset_corner(
449                           quarter_round ?
450                             [
451                              [quarter_round_ofs,radius],
452                              [0,0],
453                              [quarter_round_ofs+radius,0]
454                             ]
455                           : mask_angle>90 && flat_top && is_undef(bulge) ? 
456                            [
457                              [radius/tan(mask_angle),radius],
458                              [0,0],
459                              [radius,0]
460                            ]
461                           :
462                            [
463                              radius*[cos(mask_angle),sin(mask_angle)],
464                              [0,0],
465                              [radius,0]
466                            ],
467                            mask_angle, inset, excess, flat_top),
468        quarter_round_big_fix = quarter_round && mask_angle>135 ? quarter_round_ofs+radius
469                      : 0,
470        flatfix = !quarter_round && is_undef(bulge) && flat_top && mask_angle>90 ? radius/tan(mask_angle)
471                  : 0,
472        corners = select(outside_corner[1], [0,2]) - [[quarter_round_big_fix+flatfix,0],[quarter_round_big_fix,0]],
473        bulgept = is_undef(bulge) ? undef
474                : let(
475                      normal = line_normal(corners)
476                  )
477                  mean(corners)+bulge*normal,
478        dummy=assert(corners[1].x>=0, str("inset.y is too large to fit cove at angle ",mask_angle)),
479        cp = quarter_round ? [corners[0].x,inset.y] : outside_corner[1][1],
480        path = deduplicate([
481                            [corners[1].x,-excess],
482                            each select(outside_corner[0],1,-1),
483                            if (bulge) each arc(points=[corners[0], bulgept, corners[1]]),           
484                            if (!bulge) each arc(cp=cp, points = corners),
485                           ],
486                           closed=true)
487    ) reorient(anchor,spin, two_d=true, path=path, extent=false, p=path);
488
489
490
491// Function&Module: mask2d_chamfer()
492// Synopsis: Produces a 2D chamfer mask shape.
493// SynTags: Geom, Path
494// Topics: Shapes (2D), Paths (2D), Path Generators, Attachable, Masks (2D)
495// See Also: corner_profile(), edge_profile(), face_profile()
496// Usage: As Module
497//   mask2d_chamfer(edge, [angle], [inset], [excess]) [ATTACHMENTS];
498//   mask2d_chamfer(y=, [angle=], [inset=], [excess=]) [ATTACHMENTS];
499//   mask2d_chamfer(x=, [angle=], [inset=], [excess=]) [ATTACHMENTS];
500// Usage: As Function
501//   path = mask2d_chamfer(edge, [angle], [inset], [excess]);
502//   path = mask2d_chamfer(y=, [angle=], [inset=], [excess=]);
503//   path = mask2d_chamfer(x=, [angle=], [inset=], [excess=]);
504// Description:
505//   Creates a 2D chamfer mask shape that is useful for extruding into a 3D mask for an edge. 
506//   Conversely, you can use that same extruded shape to make an interior chamfer between two walls.
507//   As a 2D mask, this is designed to be differenced away from the edge of a shape that with its corner at the origin and one edge on the X+ axis and the other mask_angle degrees counterclockwise from the X+ axis.  
508//   If called as a function, returns a 2D path of the outline of the mask shape.
509//   The edge parameter specifies the length of the chamfer's slanted edge.  The x parameter specifies the width.  The y parameter
510//   specfies the length of the non-horizontal arm of the chamfer.  The height specifies the height of the chamfer independent
511//   of angle.  You can specify any combination of parameters that determines a chamfer geometry.  
512// Arguments:
513//   edge = The length of the edge of the chamfer.
514//   angle = The angle of the chamfer edge, away from vertical.  Default: mask_angle/2.
515//   inset = Optional amount to inset perpendicular to each edge.  Scalar or 2-vector.  Default: 0
516//   mask_angle = Number of degrees in the corner angle to mask.  Default: 90
517//   excess = Extra amount of mask shape to creates on the X- and Y- sides of the shape.  Default: 0.01
518//   ---
519//   x = The width of the chamfer (joint distance in x direction)
520//   y = The set-back (joint distance) in the non-x direction of the chamfer. 
521//   h / height = The height of the chamfer (excluding inset and excess).
522//   w/ width = The width of the chamfer (excluding inset and excess).
523//   quarter_round = If true, make a roundover independent of the mask_angle, defined based on a 90 deg angle, with a constant height.  Default: false.
524//   flat_top = If true, the top inset of the mask will be horizontal instead of angled by the mask_angle.  Default: true.
525//   anchor = Translate so anchor point is at origin (0,0,0).  See [anchor](attachments.scad#subsection-anchor).  Default: `CENTER`
526//   spin = Rotate this many degrees around the Z axis after anchor.  See [spin](attachments.scad#subsection-spin).  Default: `0`
527// Side Effects:
528//  Tags the children with "remove" (and hence sets `$tag`) if no tag is already set.
529// Example(2D): 2D Chamfer Mask, at 45 deg by default
530//   mask2d_chamfer(x=10);
531// Example(2D): 2D Chamfer Mask, at 30 deg (measured down from vertical)
532//   mask2d_chamfer(x=10,angle=30);
533// Example(2D): 2D Chamfer Mask on an acute angle.  The default chamfer angle is to produce a symmetric chamfer.  
534//   mask2d_chamfer(x=10,mask_angle=45);
535// Example(2D): 2D Chamfer Mask on an acute angle.  Here we specify the angle of the chamfer
536//   mask2d_chamfer(x=10,mask_angle=45,angle=45);
537// Example(2D): 2D Chamfer Mask specified by x and y length
538//   mask2d_chamfer(x=4,y=10);
539// Example(2D): 2D Chamfer Mask specified by x and y length.  The y length is along the top side of the chamfer, not parallel to the Y axis.
540//   mask2d_chamfer(x=4,y=5,mask_angle=44);
541// Example(2D): 2D Chamfer Mask specified by width and height.  
542//   mask2d_chamfer(w=4,h=5,mask_angle=44);
543// Example(2D): 2D Chamfer Mask on obtuse angle, specifying x.  The right tip is 10 units from the origin. 
544//   mask2d_chamfer(x=10,mask_angle=127);
545// Example(2D): 2D Chamfer Mask on obtuse angle, specifying width.  The entire width is 10. 
546//   mask2d_chamfer(w=10,mask_angle=127);
547// Example(2D): 2D Chamfer Mask by edge
548//    mask2d_chamfer(edge=10);
549// Example(2D): 2D Chamfer Mask by edge, acute case
550//    mask2d_chamfer(edge=10, mask_angle=44);
551// Example(2D): 2D Chamfer Mask by edge, obtuse case
552//    mask2d_chamfer(edge=10, mask_angle=144);
553// Example(2D): 2D Chamfer Mask by edge and angle
554//    mask2d_chamfer(edge=10, angle=30);
555// Example(2D): 2D Chamfer Mask by edge and x
556//    mask2d_chamfer(edge=10, x=9);
557// Example(2D): 2D Inset Chamfer Mask
558//     mask2d_chamfer(x=10, inset=2);
559// Example(2D): 2D Inset Chamfer Mask on acute angle
560//     mask2d_chamfer(x=10, inset=2, mask_angle=77);
561// Example(2D): 2D Inset Chamfer Mask on acute angle with flat top
562//     mask2d_chamfer(x=10, inset=2, mask_angle=77, flat_top=true);
563// Example: Masking by Edge Attachment
564//   diff()
565//   cube([50,60,70],center=true)
566//       edge_profile([TOP,"Z"],except=[BACK,TOP+LEFT])
567//           mask2d_chamfer(x=10, inset=2);
568// Example: Making an interior chamfer
569//   %render() difference() {
570//       move(-[5,0,5]) cube(30, anchor=BOT+LEFT);
571//       cube(310, anchor=BOT+LEFT);
572//   }
573//   xrot(90)
574//       linear_extrude(height=30, center=true)
575//           mask2d_chamfer(edge=10);
576// Example(3D,Med): Chamfering an extreme prismoid by setting height
577//   diff()
578//   prismoid([50,60], [20,30], h=20, shift=[25,16])
579//       edge_profile(TOP, excess=20)//let(f=$edge_angle)
580//           mask2d_chamfer(h=5,mask_angle=$edge_angle);
581// Example(3D,Med): Chamfering an extreme prismoid with a fixed chamfer angle.  Note that a very large chamfer angle is required because of the large obtuse angles.  
582//   diff()
583//   prismoid([50,60], [20,30], h=20, shift=[25,16])
584//       edge_profile(TOP, excess=20)//let(f=$edge_angle)
585//           mask2d_chamfer(h=5,mask_angle=$edge_angle,angle=64);
586// Example(3D,Med): Chamfering an extreme prismoid by setting height with inset and flat_top=true.
587//   diff()
588//   prismoid([50,60], [20,30], h=20, shift=[25,16])
589//       edge_profile(TOP, excess=20)//let(f=$edge_angle)
590//           mask2d_chamfer(h=4,inset=1,flat_top=true,mask_angle=$edge_angle);
591
592module mask2d_chamfer(edge, angle, inset=0, excess=0.01, mask_angle=90, flat_top=false, x, y, h, w, height, width, anchor=CENTER,spin=0) {
593    path = mask2d_chamfer(x=x, y=y, edge=edge, angle=angle, height=height, h=h, excess=excess, w=w,
594                          inset=inset, mask_angle=mask_angle, flat_top=flat_top,width=width);
595    default_tag("remove") {
596        attachable(anchor,spin, two_d=true, path=path, extent=true) {
597            polygon(path);
598            children();
599        }
600    }
601}
602
603function mask2d_chamfer(edge, angle, inset=0, excess=0.01, mask_angle=90, flat_top=false, x, y, h, w, width, height, anchor=CENTER,spin=0) =
604    assert(is_undef(x) || all_positive([x]))
605    assert(is_undef(y) || all_positive([y]))
606    assert(is_undef(w) || all_positive([w]))
607    assert(is_undef(h) || all_positive([h]))
608    assert(is_undef(height) || all_positive([height]))
609    assert(is_undef(width) || all_positive([width]))
610    assert(is_undef(edge) || all_positive([edge]))            
611    assert(all_nonnegative([excess]))
612    assert(is_finite(mask_angle) && mask_angle>0 && mask_angle<180)
613    assert(is_finite(inset)||is_vector(inset,2))
614    assert(is_undef(angle) || angle>mask_angle-90, str("angle must be larger than ",mask_angle-90," for chamfer to fit"))
615    let(
616        inset = is_list(inset)? inset : [inset,inset],
617        h = one_defined([h,height],"h,height",dflt=undef),
618        w = one_defined([w,width],"w,width",dflt=undef),
619        dummy = assert(num_defined([y,h])<=1, "Cannot defined both h / height and y")
620                assert(num_defined([x,w])<=1, "Cannot defined both w / width and x"),
621        y = is_def(h) ? assert(all_positive([h]), "height / h must be postitive")
622                        h/sin(mask_angle) : y, 
623        xy = is_def(w) ? assert(is_undef(edge), "Cannot combine edge with width")
624                         assert(num_defined([y,angle])<=1, "Conflicting values of width, y and angle given")
625                         let(
626                             angle=default(angle,mask_angle/2),
627                             y = is_def(y) ? y
628                               : w/tan(angle)
629                         )                               
630                         [w+y*cos(mask_angle),y]
631           : is_def(x) ? assert(num_defined([y,edge,angle])<=1, "Conflicting values of x, y, height, edge and angle given")
632                         (
633                             is_def(y) ? [x,y]
634                           : is_def(edge) ? let(yopt=quadratic_roots(1,-2*x*cos(mask_angle), x^2-edge^2,real=true),fff=echo(yopt))
635                                           assert(yopt!=[] && max(yopt)>0, "edge too short for x value")
636                                           [x,max(yopt)]
637                           : let(angle=default(angle,mask_angle/2))
638                             [x,law_of_sines(a=x,A=90-mask_angle+angle,B=90-angle)]
639                         )
640           : is_def(y) ? assert(num_defined([edge,angle])<=1, "Conflicting or insufficient values of x, y, height, edge and angle given")
641                         (
642                             is_def(edge) ? let(xopt=quadratic_roots(1,-2*y,cos(mask_angle), y^2-edge^2,real=true))
643                                            assert(xopt!=[], "edge too short for y value")
644                                            [x,max(xopt)]
645                           : let(angle=default(angle,mask_angle/2))
646                             [law_of_sines(a=y,A=90-angle,B=90-mask_angle+angle), y]
647                         )
648           : assert(is_def(edge), "Must give one of x, y, w/width, h/height, or edge")
649             let(angle=default(angle,mask_angle/2))
650             [law_of_sines(a=edge,A=mask_angle, B=90-mask_angle+angle),
651              law_of_sines(a=edge,A=mask_angle, B=90-angle)],
652        dummy3=assert(xy.x > xy.y*cos(mask_angle), str("Chamfer does not fit with mask_angle ",mask_angle)),
653        // These computations are just for the error message...actually only work without inset
654        // ref_pt = polar_to_xy(xy.y, mask_angle),
655        // angle = 90-atan(ref_pt.y/(xy.x-ref_pt.x)),
656        outside_corner = _inset_corner(
657                            [
658                              polar_to_xy(xy.y,mask_angle),
659                              [0,0],
660                              [xy.x,0]
661                            ],
662                            mask_angle, inset, excess, flat_top),
663        dummy2=assert(outside_corner[1][2].x>0,str("Angle of chamfer is too small to fit on mask angle ",mask_angle,
664                                                   ".  Either increase angle or add x inset to make space.")),
665        path = deduplicate(concat(outside_corner[0], select(outside_corner[1],[0,2])),closed=true)
666    ) reorient(anchor,spin, two_d=true, path=path, extent=false, p=path);
667
668
669// Function&Module: mask2d_rabbet()
670// Synopsis: Creates a rabbet mask shape.
671// SynTags: Geom, Path
672// Topics: Shapes (2D), Paths (2D), Path Generators, Attachable, Masks (2D)
673// See Also: corner_profile(), edge_profile(), face_profile()
674// Usage: As Module
675//   mask2d_rabbet(size, [mask_angle], [excess]) [ATTACHMENTS];
676// Usage: As Function
677//   path = mask2d_rabbet(size, [mask_angle], [excess]);
678// Description:
679//   Creates a 2D rabbet mask shape.  When differenced away, this mask
680//   creates at the corner a rectanguler space of the specified size.
681//   This mask can be extruding into a 3D mask for an edge, or
682//   you can use that same extruded shape to make an interior shelf decoration between two walls.
683//   As a 2D mask, this is designed to be differenced away from the edge of a shape that with its corner at the origin and one edge on the X+ axis and the other mask_angle degrees counterclockwise from the X+ axis.  
684//   If called as a function, returns a 2D path of the outline of the mask shape.
685// Arguments:
686//   size = The size of the rabbet, either as a scalar or an [X,Y] list.
687//   mask_angle = Number of degrees in the corner angle to mask.  Default: 90
688//   excess = Extra amount of mask shape to creates on the X and quasi-Y sides of the shape. Default: 0.01
689//   ---
690//   anchor = Translate so anchor point is at origin (0,0,0).  See [anchor](attachments.scad#subsection-anchor).  Default: `CENTER`
691//   spin = Rotate this many degrees around the Z axis after anchor.  See [spin](attachments.scad#subsection-spin).  Default: `0`
692// Side Effects:
693//   Tags the children with "remove" (and hence sets `$tag`) if no tag is already set.
694// Example(2D): 2D Rabbet Mask
695//   mask2d_rabbet(size=10);
696// Example(2D): 2D Asymmetrical Rabbet Mask
697//   mask2d_rabbet(size=[5,10]);
698// Example(2D): 2D Mask for a acute angle edge
699//   mask2d_rabbet(size=10, mask_angle=75);
700// Example(2D): 2D Mask for obtuse angle edge.  If the obtuse angle is too large the rabbet will not fit.  If that happens, you will need to increase the rabbet width.  
701//   mask2d_rabbet(size=10, mask_angle=125);
702// Example: Masking by Edge Attachment
703//   diff()
704//   cube([50,60,70],center=true)
705//       edge_profile([TOP,"Z"],except=[BACK,TOP+LEFT])
706//           mask2d_rabbet(size=10);
707// Example: Making an interior shelf
708//   %render() difference() {
709//       move(-[5,0,5]) cube(30, anchor=BOT+LEFT);
710//       cube(310, anchor=BOT+LEFT);
711//   }
712//   xrot(90)
713//       linear_extrude(height=30, center=true)
714//           mask2d_rabbet(size=[5,10]);
715module mask2d_rabbet(size, mask_angle=90, excess=0.01, anchor=CTR, spin=0) {
716    path = mask2d_rabbet(size=size, mask_angle=mask_angle, excess=excess);
717    default_tag("remove") {
718        attachable(anchor,spin, two_d=true, path=path, extent=false) {
719            polygon(path);
720            children();
721        }
722    }
723}
724
725
726
727function mask2d_rabbet(size, mask_angle=90, excess=0.01, anchor=CTR, spin=0) =
728    assert(is_finite(size)||is_vector(size,2))
729    assert(is_finite(mask_angle) && mask_angle>0 && mask_angle<180)
730    assert(all_nonnegative([excess]))
731    let( 
732        size = force_list(size,2),
733        top = polar_to_xy(size.y/sin(mask_angle),mask_angle),
734        bot = [top.x+size.x,0],
735        dummy=assert(top.x+size.x>=0, str("Rabbet of size ",size, " does not fit on ",mask_angle," corner.")),
736        outside_corner = _inset_corner([top,[0,0],bot],mask_angle, [0,0], excess, flat_top=true),
737        path =deduplicate([
738                each outside_corner[0],
739                outside_corner[1][0],
740                [outside_corner[1][2].x, outside_corner[1][0].y],
741                outside_corner[1][2]
742                ],closed=true)
743    ) reorient(anchor,spin, two_d=true, path=path, extent=false, p=path);
744
745
746// Function&Module: mask2d_dovetail()
747// Synopsis: Creates a 2D dovetail mask shape.
748// SynTags: Geom, Path
749// Topics: Masks (2D), Shapes (2D), Paths (2D), Path Generators, Attachable 
750// See Also: corner_profile(), edge_profile(), face_profile()
751// Usage: As Module
752//   mask2d_dovetail(edge, angle, [inset], [shelf], [excess], ...) [ATTACHMENTS];
753//   mask2d_dovetail(width=, angle=, [inset=], [shelf=], [excess=], ...) [ATTACHMENTS];
754//   mask2d_dovetail(height=, angle=, [inset=], [shelf=], [excess=], ...) [ATTACHMENTS];
755//   mask2d_dovetail(width=, height=, [inset=], [shelf=], [excess=], ...) [ATTACHMENTS];
756// Usage: As Function
757//   path = mask2d_dovetail(edge, [angle], [inset], [shelf], [excess]);
758// Description:
759//   Creates a 2D dovetail mask shape that is useful for extruding into a 3D mask for a 90° edge.
760//   Conversely, you can use that same extruded shape to make an interior dovetail between two walls at a 90º angle.
761//   As a 2D mask, this is designed to be differenced away from the edge of a shape that with its corner at the origin and one edge on the X+ axis and the other mask_angle degrees counterclockwise from the X+ axis.  
762//   If called as a function, returns a 2D path of the outline of the mask shape.
763// Arguments:
764//   edge = The length of the edge of the dovetail.
765//   angle = The angle of the chamfer edge, away from vertical.  
766//   shelf = The extra height to add to the inside corner of the dovetail.  Default: 0
767//   inset = Optional amount to inset in perpendicular direction from each edge.  Default: 0
768//   mask_angle = Number of degrees in the corner angle to mask.  Default: 90
769//   excess = Extra amount of mask shape to creates on the X and quasi-Y sides of the shape.  Default: 0.01
770//   ---
771//   width = The width of the dovetail (excluding any inset)
772//   height = The height of the dovetail (excluding any inset or shelf). 
773//   flat_top = If true, the top inset of the mask will be horizontal instead of angled by the mask_angle.  Default: true.
774//   anchor = Translate so anchor point is at origin (0,0,0).  See [anchor](attachments.scad#subsection-anchor).  Default: `CENTER`
775//   spin = Rotate this many degrees around the Z axis after anchor.  See [spin](attachments.scad#subsection-spin).  Default: `0`
776// Side Effects:
777//  Tags the children with "remove" (and hence sets `$tag`) if no tag is already set.
778// Example(2D): 2D Dovetail Mask
779//   mask2d_dovetail(width=10,angle=14);
780// Example(2D): 2D Dovetail Mask by height and slope.  A slope of 1/6 is a common choice. 
781//   mask2d_dovetail(height=20, slope=1/6);
782// Example(2D): 2D Inset Dovetail Mask to make the dovetail wider
783//   mask2d_dovetail(width=5, angle=12, inset=[4,0]);
784// Example(2D): 2D Inset Dovetail Mask on an obtuse angle
785//   mask2d_dovetail(width=5, mask_angle=110, angle=12);
786// Example(2D): 2D Inset Dovetail Mask on an acute angle will generally require an inset in order to fit.  
787//   mask2d_dovetail(width=5, mask_angle=70, angle=12, inset=[6,0]);
788// Example(2D): 2D dovetail mask by edge length and angle
789//   mask2d_dovetail(edge=10,width=4);
790// Example(2D): 2D dovetail mask by width and height
791//   mask2d_dovetail(width=5,height=25);
792// Example: Masking by Edge Attachment
793//   diff()
794//   cube([50,60,70],center=true)
795//       edge_profile([TOP,"Z"],except=[BACK,TOP+LEFT])
796//           mask2d_dovetail(width=10, angle=30, inset=2);
797// Example: Making an interior dovetail
798//   %render() difference() {
799//       move(-[5,0,5]) cube(30, anchor=BOT+LEFT);
800//       cube(310, anchor=BOT+LEFT);
801//   }
802//   xrot(90)
803//       linear_extrude(height=30, center=true)
804//           mask2d_dovetail(width=10,angle=30);
805module mask2d_dovetail(edge, angle, shelf=0, inset=0, mask_angle=90, excess=0.01, flat_top=true, w,h,width,height, slope, anchor=CENTER, spin=0,x,y) {
806    path = mask2d_dovetail(w=w,width=width,h=h,height=height, edge=edge, angle=angle, inset=inset, shelf=shelf, excess=excess, slope=slope, flat_top=flat_top, mask_angle=mask_angle,x=x,y=y);
807    default_tag("remove") {
808        attachable(anchor,spin, two_d=true, path=path) {
809            polygon(path);
810            children();
811        }
812    }
813}
814
815function mask2d_dovetail(edge, angle, slope, shelf=0, inset=0, mask_angle=90, excess=0.01, flat_top=true, w,width,h,height, anchor=CENTER, spin=0,x,y) =
816    assert(num_defined([slope,angle])<=1, "Cannot give both slope and angle")
817    assert(is_finite(excess))
818    assert(is_undef(w) || all_positive([w]))
819    assert(is_undef(h) || all_positive([h]))
820    assert(is_undef(height) || all_positive([height]))
821    assert(is_undef(width) || all_positive([width]))
822    assert(is_finite(inset)||is_vector(inset,2))
823    let(
824        y = one_defined([h,height,y],"h,height,y",dflt=undef),
825        x = one_defined([w,width,x],"w,width,x",dflt=undef),
826        angle = is_def(slope) ? atan(slope) : angle,
827        dummy2=//assert(num_defined([x,y])==2 || (all_positive([angle]) && angle<90), "Invalid angle or slope")
828               assert(num_defined([x,y])<2 || is_undef(angle), "Cannot give both width and height if you give slope or angle"),
829        inset = force_list(inset,2),
830        width = is_def(x)? x
831              : is_def(y)? adj_ang_to_opp(adj=y,ang=angle)
832              : assert(all_positive([edge]))
833                hyp_ang_to_opp(hyp=edge,ang=angle),
834        height = is_def(y) ? y
835               : num_defined([width,angle])==2 ? opp_ang_to_adj(opp=width,ang=angle)+shelf
836               : all_defined([edge,angle]) ? hyp_ang_to_adj(hyp=edge,ang=angle)
837               : assert(is_def(edge) && edge>width) sqrt(edge^2-width^2),
838        top = polar_to_xy(height/sin(mask_angle),mask_angle),
839        outside_corner = _inset_corner([top,[0,0],[0,0]], mask_angle, inset, excess, flat_top),
840        dummy=assert(outside_corner[1][1].x+width > top.x, "Dovetail doesn't fit on that angled edge.  Try increasing x inset.")
841              assert(outside_corner[1][1].x>=0, "Dovetails doesn't fit on the edge.  Try decreasing y inset."),
842        path = deduplicate([
843                            each outside_corner[0],
844                            outside_corner[1][0],
845                            if (shelf>0) outside_corner[1][1]+[width,height],
846                            outside_corner[1][1]+[width,height-shelf],
847                            outside_corner[1][1]
848                           ], closed=true)
849    ) reorient(anchor,spin, two_d=true, path=path, p=path);
850
851
852
853// Function&Module: mask2d_ogee()
854// Synopsis: Creates a 2D ogee mask shape.
855// SynTags: Geom, Path
856// Topics: Shapes (2D), Paths (2D), Path Generators, Attachable, Masks (2D)
857// See Also: corner_profile(), edge_profile(), face_profile()
858// Usage: As Module
859//   mask2d_ogee(pattern, [excess], ...) [ATTAHCMENTS];
860// Usage: As Function
861//   path = mask2d_ogee(pattern, [excess], ...);
862// Description:
863//   Creates a 2D Ogee mask shape that is useful for extruding into a 3D mask for a 90° edge.
864//   Conversely, you can use that same extruded shape to make an interior ogee decoration between two walls at a 90º angle.
865//   As a 2D mask, this is designed to be differenced away from the edge of a shape that with its corner at the origin and one edge on the X+ axis and the other mask_angle degrees counterclockwise from the X+ axis.  
866//   Since there are a number of shapes that fall under the name ogee, the shape of this mask is given as a pattern.
867//   Patterns are given as TYPE, VALUE pairs.  ie: `["fillet",10, "xstep",2, "step",[5,5], ...]`.  See Patterns below.
868//   If called as a function, returns a 2D path of the outline of the mask shape.
869//   .
870//   ### Patterns
871//   .
872//   Type     | Argument  | Description
873//   -------- | --------- | ----------------
874//   "step"   | [x,y]     | Makes a line to a point `x` right and `y` down.
875//   "xstep"  | dist      | Makes a `dist` length line towards X+.
876//   "ystep"  | dist      | Makes a `dist` length line towards Y-.
877//   "round"  | radius    | Makes an arc that will mask a roundover.
878//   "fillet" | radius    | Makes an arc that will mask a fillet.
879//
880// Arguments:
881//   pattern = A list of pattern pieces to describe the Ogee.
882//   excess = Extra amount of mask shape to creates on the X- and Y- sides of the shape. Default: 0.01
883//   ---
884//   anchor = Translate so anchor point is at origin (0,0,0).  See [anchor](attachments.scad#subsection-anchor).  Default: `CENTER`
885//   spin = Rotate this many degrees around the Z axis after anchor.  See [spin](attachments.scad#subsection-spin).  Default: `0`
886//
887// Side Effects:
888//   Tags the children with "remove" (and hence sets `$tag`) if no tag is already set.
889//
890// Example(2D): 2D Ogee Mask
891//   mask2d_ogee([
892//       "xstep",1,  "ystep",1,  // Starting shoulder.
893//       "fillet",5, "round",5,  // S-curve.
894//       "ystep",1,  "xstep",1   // Ending shoulder.
895//   ]);
896// Example: Masking by Edge Attachment
897//   diff()
898//   cube([50,60,70],center=true)
899//       edge_profile(TOP)
900//           mask2d_ogee([
901//               "xstep",1,  "ystep",1,  // Starting shoulder.
902//               "fillet",5, "round",5,  // S-curve.
903//               "ystep",1,  "xstep",1   // Ending shoulder.
904//           ]);
905// Example: Making an interior ogee
906//   %render() difference() {
907//       move(-[5,0,5]) cube(30, anchor=BOT+LEFT);
908//       cube(310, anchor=BOT+LEFT);
909//   }
910//   xrot(90)
911//       linear_extrude(height=30, center=true)
912//           mask2d_ogee([
913//               "xstep", 1, "round",5,
914//               "ystep",1, "fillet",5,
915//               "xstep", 1, "ystep", 1,
916//           ]);
917module mask2d_ogee(pattern, excess=0.01, anchor=CENTER,spin=0) {
918    path = mask2d_ogee(pattern, excess=excess);
919    default_tag("remove") {
920        attachable(anchor,spin, two_d=true, path=path) {
921            polygon(path);
922            children();
923        }
924    }
925}
926
927function mask2d_ogee(pattern, excess=0.01, anchor=CENTER, spin=0) =
928    assert(is_list(pattern))
929    assert(len(pattern)>0)
930    assert(len(pattern)%2==0,"pattern must be a list of TYPE, VAL pairs.")
931    assert(all([for (i = idx(pattern,step=2)) in_list(pattern[i],["step","xstep","ystep","round","fillet"])]))
932    let(
933        x = concat([0], cumsum([
934            for (i=idx(pattern,step=2)) let(
935                type = pattern[i],
936                val = pattern[i+1]
937            ) (
938                type=="step"?   val.x :
939                type=="xstep"?  val :
940                type=="round"?  val :
941                type=="fillet"? val :
942                0
943            )
944        ])),
945        y = concat([0], cumsum([
946            for (i=idx(pattern,step=2)) let(
947                type = pattern[i],
948                val = pattern[i+1]
949            ) (
950                type=="step"?   val.y :
951                type=="ystep"?  val :
952                type=="round"?  val :
953                type=="fillet"? val :
954                0
955            )
956        ])),
957        tot_x = last(x),
958        tot_y = last(y),
959        data = [
960            for (i=idx(pattern,step=2)) let(
961                type = pattern[i],
962                val = pattern[i+1],
963                pt = [x[i/2], tot_y-y[i/2]] + (
964                    type=="step"?   [val.x,-val.y] :
965                    type=="xstep"?  [val,0] :
966                    type=="ystep"?  [0,-val] :
967                    type=="round"?  [val,0] :
968                    type=="fillet"? [0,-val] :
969                    [0,0]
970                )
971            ) [type, val, pt]
972        ],
973        path = [
974            [tot_x,-excess],
975            [-excess,-excess],
976            [-excess,tot_y],
977            for (pat = data) each
978                pat[0]=="step"?  [pat[2]] :
979                pat[0]=="xstep"? [pat[2]] :
980                pat[0]=="ystep"? [pat[2]] :
981                let(
982                    r = pat[1],
983                    steps = segs(abs(r)),
984                    step = 90/steps
985                ) [
986                    for (i=[0:1:steps]) let(
987                        a = pat[0]=="round"? (180+i*step) : (90-i*step)
988                    ) pat[2] + abs(r)*[cos(a),sin(a)]
989                ]
990        ],
991        path2 = deduplicate(path)
992    ) reorient(anchor,spin, two_d=true, path=path2, p=path2);
993
994
995// vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap